/*
 * Copyright 2011 Clean GWT
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.cleangwt.uibinder.rebind;

import java.util.ArrayList;
import java.util.List;

import com.cleangwt.uibinder.client.UiField;
import com.cleangwt.uibinder.client.UiHandler;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.rebind.SourceWriter;

/**
 * The helper class for writing codes of binding method.
 * 
 * @author Jess
 * @version 1.0
 * @since 1.0
 */
class EventHandlerWriter {
  private final JClassType eventHandlerJClass;
  private final JClassType handlerRegistrationJClass;
  private final TreeLogger logger;
  private int varCounter = 0;

  EventHandlerWriter(TreeLogger logger, TypeOracle oracle) {
    this.logger = logger;
    handlerRegistrationJClass = oracle.findType(HandlerRegistration.class.getCanonicalName());
    eventHandlerJClass = oracle.findType(EventHandler.class.getCanonicalName());
  }

  /**
   * Generates codes of handler declaration and event binding for respective fields.
   * 
   * @param writer the writer used to output the results
   * @param className the output class name
   * @param binderType the binder type provides methods with {@link UiHandler}
   * @param viewType the view type provides fields with {@link UiField}
   * @param viewName the view name to be written in {viewName}.{objectName}.addXXXHandler(...). Put
   *          null to ignore the view domain
   */
  void run(SourceWriter writer, String className, JClassType binderType, JClassType viewType,
      String viewName) throws UnableToCompleteException {
    final List<JMethod> uiHandlers = new ArrayList<JMethod>();
    String viewDomain = viewName == null ? "" : (viewName + ".");
    findActions(binderType, uiHandlers);

    // Iterate through all methods defined in the class.
    for (JMethod method : uiHandlers) {
      // Evaluate the method.
      String boundMethod = method.getName();
      if (method.isPrivate()) {
        logger.log(Type.ERROR, "Method " + boundMethod + " in "
            + binderType.getQualifiedSourceName() + " cannot be private.");
      }

      // Retrieves both event and handler types.
      JParameter[] parameters = method.getParameters();
      if (parameters.length != 1) {
        logger.log(Type.ERROR, "Method " + boundMethod + " in "
            + binderType.getQualifiedSourceName() + "must have a single event parameter defined.");
      }
      JClassType eventType = parameters[0].getType().isClass();
      if (eventType == null) {
        logger.log(Type.ERROR, "Parameter type is not a class.");
      }

      JClassType handlerType = getHandlerForEvent(eventType);
      if (handlerType == null) {
        logger.log(Type.ERROR, "Parameter '" + eventType.getName()
            + "' is not an event (subclass of GwtEvent).");
      }
      UiHandler annotation = method.getAnnotation(UiHandler.class);
      // Cool to add the handler in the output.
      String handlerVarName = "v" + (++varCounter);
      writeHandler(writer, className, handlerVarName, handlerType, eventType, boundMethod,
          viewDomain, annotation.validate());

      // Adds the handler for the view type itself.
      if (annotation.value().length == 0) {
        // Retrieves the "add handler" method in the object.
        JMethod addHandlerMethodType = getAddHandlerMethodForObject(viewType, handlerType);
        if (addHandlerMethodType == null) {
          logger.log(Type.ERROR, viewType.getQualifiedSourceName() + " does not have an 'add"
              + handlerType.getName() + "' method associated.");
        }

        // Cool to tie the handler into the view.
        writeAddHandler(writer, handlerVarName, addHandlerMethodType.getName(), "", "");
      } else {
        for (String objectName : annotation.value()) {
          // Is the field object valid?
          JField field = findField(viewType, objectName);

          if (field == null) {
            logger.log(Type.ERROR, "Field '" + objectName + "' can not be found in "
                + viewType.getQualifiedSourceName() + ".");
          }

          // Retrieves the "add handler" method in the object.
          JMethod addHandlerMethodType =
              getAddHandlerMethodForObject(field.getType().isClass(), handlerType);
          if (addHandlerMethodType == null) {
            logger.log(Type.ERROR, "Field '" + objectName + "' in "
                + viewType.getQualifiedSourceName() + " does not have an 'add"
                + handlerType.getName() + "' method associated.");
          }

          // Cool to tie the handler into the object.
          writeAddHandler(writer, handlerVarName, addHandlerMethodType.getName(), viewDomain,
              objectName + ".");
        }
      }
    }
  }

  /**
   * Scans the binder class to find all methods annotated with {@link UiHandler}, and adds them to
   * their respective fields.
   * 
   * @param binderType the binder type provides methods with {@link UiHandler}
   */
  private void findActions(JClassType binderType, List<JMethod> actionHandlers) {
    JMethod[] methods = binderType.getMethods();
    for (JMethod method : methods) {
      if (method.isAnnotationPresent(UiHandler.class)) {
        actionHandlers.add(method);
      }
    }

    // Recurse to superclass
    JClassType superclass = binderType.getSuperclass();
    if (superclass != null) {
      findActions(superclass, actionHandlers);
    }
  }

  private JField findField(JClassType viewType, String name) {
    JField field = viewType.getField(name);
    if (field == null) {
      JClassType parent = viewType.getSuperclass();
      return parent == null ? null : parent.findField(name);
    }
    return field;
  }

  /**
   * Checks if a specific handler is valid for a given object and return the method that ties them.
   * The object must override a method that returns
   * {@link com.google.gwt.event.shared.HandlerRegistration} and receives a single input parameter
   * of the same type of handlerType.
   * 
   * <p>
   * Output an error in case more than one method match the conditions described above.
   * </p>
   * 
   * <pre>
	 *   <b>Examples:</b>
	 *    - HandlerRegistration addClickHandler(ClickHandler handler)
	 *    - HandlerRegistration addMouseOverHandler(MouseOverHandler handler)
	 *    - HandlerRegistration addSubmitCompleteHandler(
	 *          FormPanel.SubmitCompleteHandler handler)
	 * </pre>
   * 
   * @param objectType the object type we want to check
   * @param handlerType the handler type we want to check in the object
   * 
   * @return the method that adds handlerType into objectType, or <b>null</b> if no method was found
   */
  private JMethod getAddHandlerMethodForObject(JClassType objectType, JClassType handlerType)
      throws UnableToCompleteException {
    JMethod handlerMethod = null;
    JMethod alternativeHandlerMethod = null;
    for (JMethod method : objectType.getInheritableMethods()) {

      // Condition 1: returns HandlerRegistration?
      if (method.getReturnType() == handlerRegistrationJClass) {

        // Condition 2: single parameter of the same type of
        // handlerType?
        JParameter[] parameters = method.getParameters();
        if (parameters.length != 1) {
          continue;
        }

        JType subjectHandler = parameters[0].getType();

        if (handlerType.equals(subjectHandler)) {

          // Condition 3: does more than one method match the
          // condition?
          if (handlerMethod != null) {
            logger.log(Type.ERROR, "This handler cannot be generated. Methods '" + method
                + "' and '" + handlerMethod + "' are ambiguous. Which one to pick?");
          }

          handlerMethod = method;
        }

        /**
         * Normalize the parameter and check for an alternative handler method. Might be the case
         * where the given objectType is generic. In this situation we need to normalize the method
         * parameter to test for equality. For instance:
         * 
         * handlerType => TableHandler<String> subjectHandler => TableHandler
         * 
         * This is done as an alternative handler method to preserve the original logic.
         */
        JParameterizedType ptype = handlerType.isParameterized();
        if (ptype != null) {
          if (subjectHandler.equals(ptype.getRawType())) {
            alternativeHandlerMethod = method;
          }
        }
      }
    }

    return (handlerMethod != null) ? handlerMethod : alternativeHandlerMethod;
  }

  /**
   * Retrieves the handler associated with the event.
   * 
   * @param eventType the given event
   * @return the associated handler, <code>null</code> if not found
   */
  private JClassType getHandlerForEvent(JClassType eventType) {

    // All handlers event must have an overrided method getAssociatedType().
    // We take advantage of this information to get the associated handler.
    // Ex:
    // com.google.gwt.event.dom.client.ClickEvent
    // ---> com.google.gwt.event.dom.client.ClickHandler
    //
    // com.google.gwt.event.dom.client.BlurEvent
    // ---> com.google.gwt.event.dom.client.BlurHandler

    if (eventType == null) {
      return null;
    }

    JMethod method = eventType.findMethod("getAssociatedType", new JType[0]);
    if (method == null) {
      logger.log(Type.WARN, "Method 'getAssociatedType()' could not be found in the event '"
          + eventType.getName() + "'.");
      return null;
    }

    JType returnType = method.getReturnType();
    if (returnType == null) {
      logger.log(Type.WARN, "The method 'getAssociatedType()' in the event '" + eventType.getName()
          + "' returns void.");
      return null;
    }

    JParameterizedType isParameterized = returnType.isParameterized();
    if (isParameterized == null) {
      logger.log(Type.WARN, "The method 'getAssociatedType()' in '" + eventType.getName()
          + "' does not return Type<? extends EventHandler>.");
      return null;
    }

    JClassType[] argTypes = isParameterized.getTypeArgs();
    if ((argTypes.length != 1) && !argTypes[0].isAssignableTo(eventHandlerJClass)) {
      logger.log(Type.WARN, "The method 'getAssociatedType()' in '" + eventType.getName()
          + "' does not return Type<? extends EventHandler>.");
      return null;
    }

    return argTypes[0];
  }

  /**
   * Writes a handler entry using the given writer.
   * 
   * @param writer the writer used to output the results
   * @param className the output class name used for namespace manipulation
   * @param handlerVarName the name of the handler variable
   * @param handlerType the handler we want to create
   * @param eventType the event associated with the handler
   * @param boundMethod the method bound in the handler
   * @param viewDomain the view domain
   * @param validator the validator should validate
   */
  private void writeHandler(SourceWriter writer, String className, String handlerVarName,
      JClassType handlerType, JClassType eventType, String boundMethod, String viewDomain,
      String validator) throws UnableToCompleteException {

    // Retrieves the single method (usually 'onSomething') related to all
    // handlers. Ex: onClick in ClickHandler, onBlur in BlurHandler ...
    JMethod[] methods = handlerType.getMethods();
    if (methods.length != 1) {
      logger.log(Type.ERROR, "'" + handlerType.getName() + "' has more than one method defined.");
    }

    // Checks if the method has an Event as parameter. Ex: ClickEvent in
    // onClick, BlurEvent in onBlur ...
    JParameter[] parameters = methods[0].getParameters();
    if (parameters.length != 1 || parameters[0].getType() != eventType) {
      logger.log(Type.ERROR, "Method '" + methods[0].getName() + "' needs '" + eventType.getName()
          + "' as parameter");
    }

    // Checks if the name of bound method is same as the handler's, that will makes a recucive call.
    if (boundMethod.equals(methods[0].getName())) {
      // Adds class namspace to prevent recucive call.
      boundMethod = className + ".this." + boundMethod;
    }

    writer.println();
    writer.println("final %1$s %2$s = new %1$s() {", handlerType
        .getParameterizedQualifiedSourceName(), handlerVarName);
    writer.indent();
    writer.println("public void %1$s(%2$s event) {", methods[0].getName(), eventType
        .getParameterizedQualifiedSourceName());
    writer.indent();
    if (!validator.isEmpty()) {
      writer.println("if(!%s%s.validate()) return;", viewDomain, validator);
    }
    writer.println("%s(event);", boundMethod);
    writer.outdent();
    writer.println("}");
    writer.outdent();
    writer.println("};");
  }

  /**
   * Adds the created handler to the given object (field) of view.
   * 
   * @param writer the writer used to output the results
   * @param handlerVarName the name of the handler variable
   * @param addHandlerMethodName the "add handler" method name associated with the object
   * @param viewDomain the view name
   * @param objectDomain the name of the object we want to tie the handler, give empty to add
   *          handler for the view itself
   */
  private void writeAddHandler(SourceWriter writer, String handlerVarName,
      String addHandlerMethodName, String viewDomain, String objectDomain) {
    writer.println("%1$s%2$s%3$s(%4$s);", viewDomain, objectDomain, addHandlerMethodName,
        handlerVarName);
  }

}
